iT邦幫忙

2026 iThome 鐵人賽

DAY 26
0
Software Development

文藝復興:這段程式碼,好像有點味道系列 第 26

Day 26|學徒天天跑去隔壁畫室幫忙調色:依戀情結 (Feature Envy)

  • 分享至 

  • xImage
  •  

文藝復興的畫室,是一個分工細密的組織
師傅負責構圖、定調;資深學徒處理背景與衣褶;新來的學徒,負責磨顏料、打底

這套分工能運作,靠一條不成文的默契
每個人做好自己那一部分,不多管別人的畫布,也不用擔心自己的畫布被別人亂動

模組四處理的是「贅肉」,模組五不一樣,處理的是「關係」
類別與類別之間,該保持多少距離,才不會綁死彼此

耦合本身不是壞事,問題出在耦合的方式不對

四個「關係」警訊:

Day Code Smell 一句話定位
26 依戀情結 (Feature Envy) 一個方法,對別的類別的資料,比對自己的資料還熟
27 不適當的親密關係 (Inappropriate Intimacy) 兩個類別,深入了解彼此到不該知道的程度
28 訊息鏈 (Message Chains) 一句請求,要經過好幾手,才能送到真正該處理它的人手上
29 中間人 (Middle Man) 一個角色,只負責轉手,自己不做任何判斷

最後一個模組,第一站,從最容易被忽略的一種開始


畫室分配給每位學徒固定的工作區

自己的顏料、自己的畫架,理論上,一整天的時間都該花在自己分內的畫布上

但如果某位學徒,每天大半時間都泡在隔壁畫架旁,幫別人調顏色、挑筆刷,自己的畫布反而空在那裡沒進度

旁人多半會問一句:「這個人,到底是誰的學徒?」

一個幾乎不碰自己資料的方法

系統裡有一個 OrderProcessor,其中一個方法,是拼出客戶的會員徽章文字:

public class Customer
{
    public string Name { get; set; }
    public CustomerTier Tier { get; set; }
    public decimal TotalSpend { get; set; }
    public int LoyaltyPoints { get; set; }
}

public class OrderProcessor
{
    public string BuildLoyaltyBadge(Customer customer)
    {
        string tierLabel = customer.Tier == CustomerTier.Vip ? "VIP" : "一般會員";

        string spendLabel = customer.TotalSpend > 100000
            ? "鑽石"
            : customer.TotalSpend > 50000
                ? "黃金"
                : "一般";

        return $"{tierLabel}・{spendLabel}・{customer.LoyaltyPoints}點";
    }
}

這個方法叫 BuildLoyaltyBadge,寫在 OrderProcessor
但整段邏輯裡,完全沒有用到 OrderProcessor 自己的任何資料

它做的每一件事,都是在讀 customer 的欄位
TierTotalSpendLoyaltyPoints 一個接一個地問

邏輯放錯了畫架

這個方法讀起來,更像是屬於 Customer 的一部分,而不是 OrderProcessor 的一部分

它帶來幾個實際的麻煩:

  • Customer 的內部欄位,只要調整一個名稱,就得回頭修改 OrderProcessor 裡這段完全不相關的程式碼
  • 未來有人想找「會員徽章是怎麼決定的」,第一直覺會去翻 Customer,卻要繞到 OrderProcessor 才找得到答案
  • OrderProcessor 承擔了一份不屬於它的知識,它原本該專心處理訂單,卻要順便搞懂會員分級的所有細節

這正是依戀情結的典型症狀:一個方法,對別人的資料比對自己的資料還熟
它天天往隔壁畫架跑,自己畫布上的工作,反而沒人記得它原本該做什麼

把邏輯,還給真正該負責的人

把銘傳大學還給劉銘傳
把亞洲大學還給亞洲統神
把成功大學還給鄭成功
把逢甲大學還給丘逢甲
把中正大學還給蔣中正
把大同大學還給葉大同

解法是搬移方法 (Move Method):把這段邏輯,整段搬到它真正依戀的 Customer

public class Customer
{
    public string Name { get; set; }
    public CustomerTier Tier { get; set; }
    public decimal TotalSpend { get; set; }
    public int LoyaltyPoints { get; set; }

    public string BuildLoyaltyBadge()
    {
        string tierLabel = Tier == CustomerTier.Vip ? "VIP" : "一般會員";

        string spendLabel = TotalSpend > 100000
            ? "鑽石"
            : TotalSpend > 50000
                ? "黃金"
                : "一般";

        return $"{tierLabel}・{spendLabel}・{LoyaltyPoints}點";
    }
}

OrderProcessor 現在只需要告知 Customer 去做這件事,不再自己動手:

public class OrderProcessor
{
    public string BuildLoyaltyBadge(Customer customer) => customer.BuildLoyaltyBadge();
}

這是「Tell, Don't Ask」原則的具體展現
與其向 Customer 問一堆欄位、自己在外面拼湊答案
不如直接告訴 Customer:「你自己算出你的徽章」

依戀情結,通常是重構後的副作用

這個壞味道,很少是一開始就故意寫成這樣的

最常見的成因,是先前做過一次資料重構

例如把散落的欄位提煉成一個新類別(呼應 Day 05 的原始型別執念、Day 07 的資料泥團)
卻只搬走了資料,忘了把操作這些資料的行為一起搬過去

資料搬家了,行為卻留在原地,繼續伸長手去操作已經搬走的資料
這條線,就是依戀情結的耦合

什麼時候,「依戀」是刻意設計

  • 策略模式 (Strategy)訪問者模式 (Visitor)
    • 這類設計,本來就是刻意把演算法跟它操作的資料分開,為了換取更高的彈性
    • 這種情況下的「依戀」,是深思熟慮的設計決定,不是意外
  • 判斷的關鍵不是「有沒有存取別人的資料」,是這個依戀,是不是有明確的設計理由,還是單純忘了物歸原主

自我檢查清單

  1. 這個方法裡,存取參數物件欄位的次數,是不是比存取自己欄位的次數還多?
  2. 如果要找這段邏輯,我的第一直覺,會去哪個類別找?現在它是不是放在別的地方?
  3. 這段邏輯,是不是曾經歷過一次資料搬家,卻只搬了資料、沒搬行為?
  4. 我是不是在寫一堆 .Get 之後自己組裝結果,而不是直接告訴物件去做?
  5. 這個「依戀」,是刻意的設計模式選擇,還是單純放錯了位置?

明日預告

明天我們看更嚴重的一種黏著:兩個類別,不只互相熟悉,還深入了解對方的每一個內部細節

模組五第二站:不適當的親密關係(Inappropriate Intimacy)


上一篇
Day 25|清倉日:敢刪,比敢加更需要判斷力
下一篇
Day 27|兩間畫室共用一把鑰匙:不適當的親密關係 (Inappropriate Intimacy)
系列文
文藝復興:這段程式碼,好像有點味道27
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言